/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

package au.com.lastweekend.jim.controller;

import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.InputEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.TooManyListenersException;

import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.TransferHandler;

import org.apache.log4j.Logger;

import au.com.lastweekend.jim.model.ContactSheetListItem;
import au.com.lastweekend.jim.model.ContactSheetListModel;
import au.com.lastweekend.jim.swing.CutCopyOwner;
import au.com.lastweekend.jim.swing.FileSelection;
import au.com.lastweekend.jim.util.FileOps;
import au.com.lastweekend.jim.view.ContactSheet;

/**
 * 
 * Controller function connecting the view and the model and the underlying domain work, in this case moving files around.
 * 
 * Notes: If a file is moved outside of the app then it will be "lost" to the imagebase. ie retrieved in a search but not found. If
 * moved inside, then we can update the imagebase.
 * 
 * @author grant@lastweekend.com.au
 * @version $Id: ContactSheetTransferHandler.java,v 1.3 2006/04/16 07:36:41 ggardner Exp $
 */
public class ContactSheetDnDController extends TransferHandler implements DropTargetListener, ClipboardOwner {

    private static final long serialVersionUID = 1L;

    private static final Logger LOG = Logger.getLogger(ContactSheetDnDController.class);

    private static final int ACTION_PASTE = 0x8;

    private int dropAction = ACTION_PASTE;

    private Transferable previousTransferable;

    private File dropFile;

    private WorkController parentController;

    public ContactSheetDnDController(WorkController controller) {

        super();
        this.parentController = controller;
    }

    @Override
    public int getSourceActions(JComponent c) {

        return COPY | MOVE;
    }

    @Override
    protected Transferable createTransferable(JComponent c) {

        JList list = (JList) c;
        ContactSheetListModel listModel = (ContactSheetListModel) list.getModel();
        int[] selectedIndices = list.getSelectedIndices();

        List<File> files = new ArrayList<File>();
        for (int i = 0; i < selectedIndices.length; i++) {

            ContactSheetListItem item = (ContactSheetListItem) listModel.getListItemAt(selectedIndices[i]);
            files.add(item.getFile());
        }

        return new ContactSheetTransferable(listModel, files);

    }

    @Override
    public void exportAsDrag(JComponent comp, InputEvent e, int action) {

        LOG.debug("exportAsDrag()");
        super.exportAsDrag(comp, e, action);
    }

    @Override
    public void exportToClipboard(JComponent comp, Clipboard clip, int action) {

        LOG.debug("exportToClipboard()");

        boolean exportSuccess = false;
        Transferable t = null;

        int clipboardAction = getSourceActions(comp) & action;
        if (clipboardAction != NONE) {
            t = createTransferable(comp);
            if (t != null) {

                // All this is to handle when we cut/copy from this
                // handler twice in a row...
                if (previousTransferable != null) {
                    lostOwnership(clip, previousTransferable);
                }

                previousTransferable = t;
                clip.setContents(t, this);

                if (t.isDataFlavorSupported(CutCopyOwner.DATA_FLAVOUR)) {
                    try {
                        ((CutCopyOwner) t.getTransferData(CutCopyOwner.DATA_FLAVOUR)).setPasteAction(action);
                    } catch (Exception e) {
                        LOG.warn("Unexpected error setting paste action", e);
                    }
                }

                exportSuccess = true;
            }
        }

        if (exportSuccess) {
            exportDone(comp, t, clipboardAction);
        } else {
            exportDone(comp, null, NONE);
        }
    }

    /*
     * For a CUT/COPY this is done as the data is put on the clipboard.
     * 
     * Model should be refreshed to include the cut/copy state.
     * 
     * For DROP (move/copy) this is done AFTER the import data <br>
     * 
     * Model is refreshed in case files have moved
     */
    @Override
    protected void exportDone(JComponent source, Transferable data, int action) {

        LOG.debug("exportDone:" + action);

        super.exportDone(source, data, action);
    }

    @Override
    public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {

        for (int i = 0; i < transferFlavors.length; i++) {
            if (FileSelection.FILE_LIST_FLAVOUR.equals(transferFlavors[i])) {
                return true;
            }
        }
        return false;

    }

    /*
     * The model for the ContactSheet being dropped is called upon to do the move/copy and can thus refresh itself.
     * 
     * _dropAction can tell if this is a DROP or a PASTE (Assuming the same thread does both, then drag exit() or drop action
     * changed should be called clearing _dropAction.
     * 
     * For DROP _dropAction contains whether this is a move or a copy
     * 
     * For PASTE, the transferable has to know if it was Cut rather than copied.
     * 
     * AND the transferable has to be notified that the cut is complete.
     * 
     * @see javax.swing.TransferHandler#importData(javax.swing.JComponent, java.awt.datatransfer.Transferable)
     */
    @Override
    public boolean importData(JComponent comp, final Transferable t) {

        final ContactSheetListModel model = getModel(comp);
        JList list = (JList) comp;

        int action = dropAction;

        CutCopyOwner cutCopyOwner = null;
        if (action == ACTION_PASTE) {
            // ensure we exit here with a valid action...
            action = DnDConstants.ACTION_NONE;

            int selectedIndex = list.getSelectedIndex();

            if (selectedIndex >= 0 && allowsDrop(model.getListItemAt(selectedIndex))
                    && t.isDataFlavorSupported(CutCopyOwner.DATA_FLAVOUR)) {
                try {
                    cutCopyOwner = (CutCopyOwner) t.getTransferData(CutCopyOwner.DATA_FLAVOUR);
                    action = cutCopyOwner.getAction();
                } catch (Exception e) {
                    action = DnDConstants.ACTION_COPY;
                    LOG.error("Unable to verify COPY/MOVE, using COPY", e);
                }
            } else {
                // Pasting to nowhere, abort.
                return false;
            }
        }

        LOG.debug("importData():" + dropAction);
        final File importTo = dropFile;
        dropFile = null;
        final int runnableAction = action;
        final CutCopyOwner runnableCutCopyOwner = cutCopyOwner;

        try {
            final List<File> fileList = getTransferDataAsListOfFiles(t);

            Runnable moveCopyRunner = new Runnable() {

                public void run() {

                    try {
                        if (runnableAction == DnDConstants.ACTION_MOVE) {
                            FileOps.INSTANCE.move(fileList, importTo);
                            if (t.isDataFlavorSupported(ContactSheetTransferable.CONTACT_SHEET_DATA_FLAVOUR)) {
                                //TODO this is still broken.
                                ContactSheetListModel model = (ContactSheetListModel) t
                                        .getTransferData(ContactSheetTransferable.CONTACT_SHEET_DATA_FLAVOUR);
                                model.reload();
                            }

                        } else {
                            FileOps.INSTANCE.copy(fileList, importTo);
                        }

                    } catch (IOException e) {
                        LOG.error("Failed to " + (runnableAction == DnDConstants.ACTION_MOVE ? "move" : "copy") + " files to "
                                + importTo, e);
                    } catch (UnsupportedFlavorException e) {
                        LOG.error("Broken transferable - " + t, e);
                    }

                }
            };

            parentController.doWork(moveCopyRunner);

            if (runnableCutCopyOwner != null) {
                runnableCutCopyOwner.pasteDone(true);
            }
            return true;
        } catch (UnsupportedFlavorException e) {
            LOG.error("Transferable doesn't support files, how did we get here?", e);
        } catch (IOException e) {
            LOG.warn("Unable to " + (action == DnDConstants.ACTION_MOVE ? "move" : "copy") + " files to " + importTo, e);
        }

        if (runnableCutCopyOwner != null) {
            runnableCutCopyOwner.pasteDone(false);
        }

        return false;
    }

    /*
     * @see java.awt.dnd.DropTargetListener#dragEnter(java.awt.dnd.DropTargetDragEvent)
     */
    public void dragEnter(DropTargetDragEvent dtde) {

        LOG.debug("dragEnter()");
        dragOver(dtde);

    }

    /*
     * @see java.awt.dnd.DropTargetListener#dragOver(java.awt.dnd.DropTargetDragEvent)
     */
    public void dragOver(DropTargetDragEvent dtde) {

        dropAction = dtde.getDropAction();
        Point dropPoint = dtde.getLocation();

        LOG.debug("dragOver(" + dropPoint + ")");

        JList l = (JList) dtde.getDropTargetContext().getComponent();
        ContactSheetListModel model = getModel(l);
        int index = l.locationToIndex(dropPoint);
        boolean inside = index > 0 && l.getCellBounds(index, index).contains(dropPoint);

        ContactSheetListItem dropItem = model.getListItemAt(index);
        if (inside && allowsDrop(dropItem)) {
            // accept drag by default
        } else {
            resetDrag();
            dtde.rejectDrag();
        }

        int cellHeight = l.getFixedCellWidth();
        int cellWidth = l.getFixedCellWidth();

        l.scrollRectToVisible(new Rectangle(dropPoint.x - cellWidth / 2, dropPoint.y - cellHeight / 2, cellWidth, cellHeight));

    }

    /**
     * @param dropItem
     * @return
     */
    private boolean allowsDrop(ContactSheetListItem dropItem) {

        dropFile = (dropItem == null ? null : dropItem.getFile());
        return dropFile != null && dropFile.isDirectory();

    }

    /*
     * @see java.awt.dnd.DropTargetListener#dropActionChanged(java.awt.dnd.DropTargetDragEvent)
     */
    public void dropActionChanged(DropTargetDragEvent dtde) {

        LOG.debug("dropActionChanged()");
        dropAction = dtde.getDropAction();

    }

    /*
     * This seems to be called AFTER the transfer handler has handled the drop via importData()
     * 
     * @see java.awt.dnd.DropTargetListener#drop(java.awt.dnd.DropTargetDropEvent)
     */
    public void drop(DropTargetDropEvent dtde) {

        LOG.debug("drop()");
        resetDrag();
    }

    private void resetDrag() {

        dropAction = ACTION_PASTE;
    }

    /*
     * @see java.awt.dnd.DropTargetListener#dragExit(java.awt.dnd.DropTargetEvent)
     */
    public void dragExit(DropTargetEvent dte) {

        LOG.debug("dragExit()");
        resetDrag();

    }

    /*
     * @see java.awt.datatransfer.ClipboardOwner#lostOwnership(java.awt.datatransfer.Clipboard, java.awt.datatransfer.Transferable)
     */
    public void lostOwnership(Clipboard clipboard, Transferable contents) {

        LOG.debug("lostOwnership()");
        if (contents.isDataFlavorSupported(CutCopyOwner.DATA_FLAVOUR)) {
            try {
                CutCopyOwner cutCopyOwner = (CutCopyOwner) contents.getTransferData(CutCopyOwner.DATA_FLAVOUR);
                cutCopyOwner.pasteDone(false);
            } catch (Exception e) {
                LOG.warn("Unexpected error notifying cutCopyOwner of aborted paste");
            }

        }
        // Stupidly we have to keep track of this in case this object sets the
        // clipboard twice in a row...
        previousTransferable = null;
    }

    private ContactSheetListModel getModel(Component comp) {

        return (ContactSheetListModel) ((JList) comp).getModel();
    }

    @SuppressWarnings("unchecked")
    private List<File> getTransferDataAsListOfFiles(Transferable t) throws UnsupportedFlavorException, IOException {

        List<File> transferData = (List<File>) t.getTransferData(FileSelection.FILE_LIST_FLAVOUR);
        return transferData;
    }

    /**
     * @param newSheet
     */
    public void addTo(ContactSheet newSheet) {

        JList view = newSheet.getViewList();
        view.setDragEnabled(true);
        view.setTransferHandler(this);
        try {
            view.getDropTarget().addDropTargetListener(this);
        } catch (TooManyListenersException e) {
            // not expecting to ever get this.
            LOG.fatal("What the?", e);
        }

    }
}